07 — 全屏与自适应调整

Three.js Journey · Bruno Simon · 难度:简单

07 — 全屏与自适应调整 课程海报

介绍

我们的 canvas 目前有一个固定的分辨率 800x600。你并不一定需要让 WebGL 铺满整个屏幕,但如果你想要沉浸式体验,那可能更好。

首先,我们希望 canvas 占据所有可用空间。然后,我们需要确保在用户调整窗口大小后它仍然适配。最后,我们需要给用户一种体验全屏模式的方式。

准备工作

starter 文件里包含我们在上一课完成的内容。我们的立方体在中央,可以拖放来移动相机。

填满视口

要让 canvas 完美填满视口,不使用 sizes 变量里的固定数字,而是使用 window.innerWidth 和 window.innerHeight:

// ...

// Sizes
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}

// ...

你可以看到 canvas 现在有了视口的宽和高。遗憾的是,有一圈白色的外边距和一个滚动条(如果你看不到滚动条,试着滚动一下)。

问题在于,浏览器都有各自的默认样式,比如标题更大、链接带下划线、段落之间有间距、页面带 padding。修复这个问题有很多方式,而且可能取决于你网站的其余部分。如果你还有其他内容,做这些的时候尽量不要破坏它们。

我们会保持简单,用 CSS 固定 canvas 的位置。

我们的 HTML 已经在加载 src/style.css 文件了:

<link rel="stylesheet" href="./style.css">

你可以像平时一样写标准 CSS,页面会自动重新加载。

首先该做的好事是,用通配符 * 移除所有元素上任何类型的 margin 或 padding:

*
{
    margin: 0;
    padding: 0;
}

然后,我们可以用它的 webgl class 选中 canvas 并把它固定在左上角:

.webgl
{
    position: fixed;
    top: 0;
    left: 0;
}

你不需要在 canvas 上指定 width 或 height,因为当你调用 renderer.setSize(...) 方法时,Three.js 已经在处理这些了。

这也是修复我们 canvas 上一个小问题的好机会。也许你注意到了在拖放时它有一个蓝色描边。这主要发生在最新版本的 Chrome 上。要修复它,我们只需给 .webgl 加上一个 outline: none;:

.webgl
{
    position: fixed;
    top: 0;
    left: 0;
    outline: none;
}

如果你想在触摸屏上也移除任何类型的滚动,你可以在 html 和 body 上加一个 overflow: hidden:

html,
body
{
    overflow: hidden;
}

现在你可以尽情欣赏你的 WebGL 了。遗憾的是,如果你调整窗口大小,canvas 不会跟随。

我们需要处理自适应调整。

处理自适应调整

要调整 canvas 的大小,我们首先需要知道窗口何时被调整。为此,你可以监听 window 上的 resize 事件。

在 sizes 变量之后马上添加 resize 监听器:

window.addEventListener('resize', () =>
{
    console.log('window has been resized')
})

现在,当窗口缩放时会触发一个函数,我们需要更新代码里的几样东西。

首先,我们必须更新 sizes 变量:

window.addEventListener('resize', () =>
{
    // Update sizes
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight
})

其次,通过修改它的 aspect 属性,我们必须更新 camera 的纵横比:

window.addEventListener('resize', () =>
{
    // ...

    // Update camera
    camera.aspect = sizes.width / sizes.height
})

当你修改像 aspect 这样的相机属性时,还需要用 camera.updateProjectionMatrix() 更新投影矩阵。我们以后会讲 matrix:

window.addEventListener('resize', () =>
{
    // ...

    camera.updateProjectionMatrix()
})

最后,我们必须更新 renderer。更新渲染器会自动更新 canvas 的宽度和高度:

window.addEventListener('resize', () =>
{
    // ...

    // Update renderer
    renderer.setSize(sizes.width, sizes.height)
})

全部合在一起:

window.addEventListener('resize', () =>
{
    // Update sizes
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight

    // Update camera
    camera.aspect = sizes.width / sizes.height
    camera.updateProjectionMatrix()

    // Update renderer
    renderer.setSize(sizes.width, sizes.height)
})
lesson demo video preview

你可以随意缩放窗口,canvas 应该覆盖整个视口,没有任何滚动条或溢出。

处理像素比

你们中的一些人可能会看到一种模糊的渲染,以及边缘上像楼梯一样的锯齿影迹(叫做 aliasing 锯齿),但并非所有人都会。如果你看到了,那是因为你在一个像素比大于 1 的屏幕上测试。

像素比对应的是:软件层面的一个像素单位,对应屏幕上多少个物理像素。

一点历史

几年前,所有屏幕的像素比都是 1,一切都很好。但你凑近看屏幕时,能看到那些像素,这限制了图像能有多精细、字体能有多细。

在这方面做得最多的公司是苹果。苹果看到了机会,开始制造像素比为 2 的屏幕,叫做 retina(视网膜屏)。现在,很多制造商都在这样做,你甚至能看到像素比更高的屏幕。

虽然这对图像质量是件好事,但像素比 2 意味着要渲染多 4 倍的像素。而像素比 3 意味着多 9 倍的像素。

你猜怎么着?最高的像素比通常出现在最弱小的设备上——也就是手机上。

你的帧率就这样没了。

处理像素比

要获取屏幕像素比,你可以用 window.devicePixelRatio,而要更新渲染器的像素比,只需要调用 renderer.setPixelRatio(...)。

你可能很想直接把设备像素比传给这个方法,但在高像素比设备上你最终会碰到性能问题。

像素比大于 2 大多是营销噱头。你的眼睛几乎看不出 2 和 3 的区别,但它会造成性能问题,还更快掉电。你能做的是把像素比限制在 2。要做到这一点,可以用 Math.min():

renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

有些技巧可以在像素比变化时得到通知,但这只涉及那些拥有多种像素比的多屏幕用户,而且他们在从一个屏幕切到另一个屏幕时通常会调整窗口大小。这就是为什么我们也会把这个方法加进 resize 回调:

window.addEventListener('resize', () =>
{
    // Update sizes
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight

    // Update camera
    camera.aspect = sizes.width / sizes.height

    // Update renderer
    renderer.setSize(sizes.width, sizes.height)
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})

处理全屏

现在我们的 canvas 以正确的像素比占据了所有可用空间,是时候添加全屏支持了。

首先,我们需要决定什么操作会触发全屏模式。它可以是一个 HTML 按钮,但相反,我们打算使用双击。

当双击发生时,我们会切换全屏——意思是,如果窗口不在全屏,双击会启用全屏模式;如果窗口已经在全屏,双击会退出全屏模式。

首先,我们需要监听双击事件,我们可以用 dblclick 事件来做:

window.addEventListener('dblclick', () =>
{
    console.log('double click')
})

这个事件在大多数现代浏览器上都能工作,除了 Chrome Android:https://developer.mozilla.org/docs/Web/API/Element/dblclick_event

现在我们有了事件,还需要三样东西:

要得知我们是否已经在全屏,我们可以用 document.fullscreenElement:

window.addEventListener('dblclick', () =>
{
    if(!document.fullscreenElement)
    {
        console.log('go fullscreen')
    }
    else
    {
        console.log('leave fullscreen')
    }
})

请求全屏的方法与元素绑定。这是因为你可以选择让什么进入全屏。它可以是整个页面、任意 DOM 元素或 <canvas>。

我们会使用 <canvas>,并在它上面调用 requestFullscreen() 方法:

window.addEventListener('dblclick', () =>
{
    if(!document.fullscreenElement)
    {
        canvas.requestFullscreen()
    }
    else
    {
        console.log('leave fullscreen')
    }
})

退出全屏模式的方法直接在 document 上可用:

window.addEventListener('dblclick', () =>
{
    if(!document.fullscreenElement)
    {
        canvas.requestFullscreen()
    }
    else
    {
        document.exitFullscreen()
    }
})
lesson demo video preview

你可以通过在任意位置双击来测试切换全屏模式。

这个浏览器在正式支持像全屏这样简单的功能上一直拖拖拉拉,我们需要使用带前缀的版本才能让 document.fullscreenElement、canvas.requestFullscreen 和 document.exitFullscreen 正常工作:

window.addEventListener('dblclick', () =>
{
    const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement

    if(!fullscreenElement)
    {
        if(canvas.requestFullscreen)
        {
            canvas.requestFullscreen()
        }
        else if(canvas.webkitRequestFullscreen)
        {
            canvas.webkitRequestFullscreen()
        }
    }
    else
    {
        if(document.exitFullscreen)
        {
            document.exitFullscreen()
        }
        else if(document.webkitExitFullscreen)
        {
            document.webkitExitFullscreen()
        }
    }
})

在所有现代浏览器上,一切应该都能正常工作。